package com.pb.camera.utils;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import android.os.Handler;
import android.os.Message;
import android.util.SparseArray;

/**
 * helper that make UI schedule async tasks easier.
 */
public final class TaskHelper {
    private static final String TAG = "TaskHelper";

    // the default multiple-threaded thread pool's threads count (a fixed thread pool)
    private static final int THREAD_POOL_THREADS_COUNT = 5;
    private static final int THREAD_POOL_THREADS_COUNT_IMAGE_LOADER_PHOTO = 2;
    private static final int THREAD_POOL_THREADS_COUNT_IMAGE_LOADER_OTHER = 2;
    private static final int THREAD_POOL_MAX_CACHE_POOL_SIZE_PHOTO = 24;
    private static final int THREAD_POOL_MAX_CACHE_POOL_SIZE_OTHER = 48;
    private static final int MSG_TYPE_CALLBACK = 1;

    private static boolean mIsDebugVersion = false;

    public static abstract class RunnableWithName {
        private String mName;
        private Runnable mRunnable;

        public RunnableWithName(String name) {
            mName = name;
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    if (mName != null)
                        Thread.currentThread().setName(mName);
                    execute();
                }
            };
        }

        public Runnable getRunnable() {
            return mRunnable;
        }

        /**
         * execute() are executed in thread pool worker thread.
         */
        public abstract void execute();
    }

    /**
     * general async task object.
     * this help UI codes easily implement some background job
     * (with an UI callback after background job complete).
     */
    public static abstract class Task {
        /**
         * execute() are executed in thread pool worker thread.
         * if exception thrown, the exception will be passed to callback() method.
         */
        public abstract void execute() throws Exception;

        /**
         * callback() are executed in UI thread
         * @param e the exception thrown by execute() (if any), may be null
         */
        public abstract void callback(Exception e);

        // debug only fields
        private static AtomicInteger mNextSeqNo = new AtomicInteger(0);
        private int mSeqNo;

        protected boolean mSingleThread = false; // true means ConcurrencyType == SINGLE, otherwise MULTIPLE
        protected Future<?> mFuture = null;
        protected boolean mCancelled = false;
        protected Exception mError = null;

        public Task() {
            if (Executor.mDebug)
                mSeqNo = mNextSeqNo.incrementAndGet();
        }

        // called by execute/callback to check if task should be cancelled
        public final boolean isCancelled() {
            return mCancelled;
        }

        /**
         * cancel this async task.
         * note: task may already executed or executing.
         */
        public final void cancel(boolean interruptIfRunning) {
            mCancelled = true;

            // cancel task if pending or running in thread pool
            try {
                if (mFuture != null)
                    mFuture.cancel(interruptIfRunning);
            } catch (Exception e) {
                Logger.w(TAG, e.toString());
            }

            // cancel task if pending for UI callback
            mHandler.removeMessages(MSG_TYPE_CALLBACK, this);
        }
    }

    // special async task that only have a callback (no background job)
    public static abstract class UITask extends Task {
        @Override
        public void execute() {}
    }

    // a fixed thread pool with unbounded work queue which task can be delayed before execute
    private static class Executor extends ScheduledThreadPoolExecutor {

        // for debug async tasks concurrent executing internals
        private static boolean mDebug = false;

        // debug only fields
        private static SparseArray<String> mRunnables = null;

        static {
            if (mDebug)
                mRunnables = new SparseArray<String>();
        }

        public Executor(int corePoolSize) {
            super(corePoolSize);
        }

        /**
         * schedule an runnable, optionally with some delay.
         * @param r the runnable
         * @param delay delay before execute runnable
         * @param tag for debug purpose, logger will use this tag when output traces.
         * @return
         */
        public Future<?> schedule(Runnable r, long delay, int seqNo, String tag) {
            if (mDebug && tag != null)
                mRunnables.put(seqNo, tag);

            return super.schedule(r, delay, TimeUnit.MILLISECONDS);
        }

        // for debug
        // note: we don't override base classes' beforeExecute because it don't use our "Runnable" when call us
        protected void beforeExecute(int seqNo) {
            if (!mDebug)
                return;

            Assert.notNull(mRunnables);
            String tag = mRunnables.get(seqNo);
            Logger.v(TAG, "before execute: " + tag);
        }

        // for debug
        // note: we don't override base classes' beforeExecute because it don't use our "Runnable" when call us
        protected void afterExecute(int seqNo, Throwable t) {
            if (!mDebug)
                return;

            Assert.notNull(mRunnables);
            String tag = mRunnables.get(seqNo);

            Logger.v(TAG, "before execute: " + tag);
            if (t != null)
                Logger.w(TAG, "after execute: " + tag + ", e = " + t.toString());
            else
                Logger.v(TAG, "after execute: " + tag);

            mRunnables.delete(seqNo);
        }
    }

    private static class FixedCountExecutor extends Executor {
        private int mMaxCachePoolSize = 100;

        public FixedCountExecutor(int corePoolSize, int cachePoolSize) {
            super(corePoolSize);
            mMaxCachePoolSize = cachePoolSize;
        }

        public void setMaxCacheSize(int size) {
            mMaxCachePoolSize = size;
        }

        public void clearTaskQueue() {
            this.getQueue().clear();
        }

        @Override
        public Future<?> schedule(Runnable r, long delay, int seqNo, String tag) {
            if (this.getQueue().size() > mMaxCachePoolSize) {
                try {
                    this.getQueue().take();
                } catch (InterruptedException e) {
                    Logger.w(TAG, e.toString());
                }
            }
            return super.schedule(r, delay, seqNo, tag);
        }

    }

    private static Handler mHandler = new Handler() {
        @SuppressWarnings("unchecked")
        @Override
        public void handleMessage(Message msg) {
            if (msg.what != MSG_TYPE_CALLBACK) {
                super.handleMessage(msg);
                return;
            }

            HardReference<Task> aTask = (HardReference<Task>)msg.obj;
            Task task = aTask.get();
            aTask.clear();

            if (task.isCancelled())
                return;

            try {
                task.callback(task.mError);
            } catch (Exception e) {
                // note: here we catch and ignore all exceptions
                Logger.w(TAG, e.toString(), e);
                if (mIsDebugVersion)
                    e.printStackTrace();
            }

            // for debug
            final Executor executor = (task.mSingleThread ? mExecutor1 : mExecutorN);
            if (Executor.mDebug)
                executor.afterExecute(task.mSeqNo, task.mError);
        }
    };

    public static enum ConcurrencyType {
        SINGLE, MULTIPLE, PLOADER, OLOADER
    }

    // single thread executor
    private static Executor mExecutor1 = new Executor(1);
    // multiple thread executor, all thread share a common fixed-count pool
    private static Executor mExecutorN = new Executor(THREAD_POOL_THREADS_COUNT);
    // multiple thread executor for UI, threads are no limited
    private static ExecutorService mExecutorZForUI = Executors.newCachedThreadPool();
    // multiple thread executor for SDK, threads are no limited
    private static ExecutorService mExecutorZForSDK = Executors.newCachedThreadPool();
    // multiple thread scheduler, threads are no limited and it is for TaskScheduler.
    private static ExecutorService mScheduler = Executors.newCachedThreadPool();

    //thread executor of image loader, a fixed-count pool, for photo items;
    private static FixedCountExecutor mExecutorP = new FixedCountExecutor(THREAD_POOL_THREADS_COUNT_IMAGE_LOADER_PHOTO, THREAD_POOL_MAX_CACHE_POOL_SIZE_PHOTO);
    //thread executor of image loader, a fixed-count pool, for other items;
    private static FixedCountExecutor mExecutorO = new FixedCountExecutor(THREAD_POOL_THREADS_COUNT_IMAGE_LOADER_OTHER, THREAD_POOL_MAX_CACHE_POOL_SIZE_OTHER);
    // single thread executor for creating mini thumbnail;
    private static FixedCountExecutor mExecutorT = new FixedCountExecutor(1, THREAD_POOL_MAX_CACHE_POOL_SIZE_PHOTO * 2);

    private TaskHelper() {}

    /**
     * cancel all async tasks and stop thread pool.
     * this should be called only once during application shutdown.
     */
    public static void shutdown() {
        mExecutor1.shutdownNow();
        mExecutorN.shutdownNow();
        mExecutorP.shutdownNow();
        mExecutorO.shutdownNow();
        mExecutorT.shutdownNow();
        mHandler.removeCallbacksAndMessages(null);
    }

    /**
     * execute an async task, immediately, all tasks will be scheduled by one thread pool (with several fixed threads).
     * @param task the async task, usually a "new Task() { ... }" or a "new UITask() { ... }"
     * @return null if can't schedule async task, otherwise the task itself.
     */
    public static Task exec(final Task task) {
        return exec(ConcurrencyType.MULTIPLE, task, 0, 0, null);
    }

    /**
     * execute an async task, immediately.
     * @param task the async task, usually a "new Task() { ... }" or a "new UITask() { ... }"
     * @param ct concurrency type, either single thread or multiple thread, for single thread type, all task will be executed one by one.
     * @return null if can't schedule async task, otherwise the task itself.
     */
    public static Task exec(ConcurrencyType ct, final Task task) {
        return exec(ct, task, 0, 0, null);
    }

    public static Task exec(ConcurrencyType ct, final Task task, String tag) {
        return exec(ct, task, 0, 0, tag);
    }

    public static Task exec(final Task task, String tag) {
        return exec(ConcurrencyType.MULTIPLE, task, 0, 0, tag);
    }

    public static Task exec(final Task task, final long delay) {
        return exec(ConcurrencyType.MULTIPLE, task, delay, 0, null);
    }

    public static Task exec(ConcurrencyType ct, final Task task, final long delay) {
        return exec(ct, task, delay, 0, null);
    }

    public static Task exec(final Task task, final long delay, String tag) {
        return exec(ConcurrencyType.MULTIPLE, task, delay, 0, tag);
    }

    public static Task exec(ConcurrencyType ct, final Task task, final long delay, String tag) {
        return exec(ct, task, delay, 0, tag);
    }

    public static Task exec(final Task task, final long backgroundDelay, final long uiDelay) {
        return exec(ConcurrencyType.MULTIPLE, task, backgroundDelay, uiDelay, null);
    }

    public static Task exec(ConcurrencyType ct, final Task task, final long backgroundDelay, final long uiDelay) {
        return exec(ct, task, backgroundDelay, uiDelay, null);
    }

    /**
     * execute an async task, with two optional delay.
     * @param ct concurrency type, either single thread or multiple thread, for single thread type, all task will be executed one by one.
     * @param task the async task, usually a "new Task() { ... }" or a "new UITask() { ... }"
     * @param backgroundDelay delay before do background work, in milliseconds, should >= 0.
     * @param uiDelay delay after do background work and before do UI callback, in milliseconds, should >= 0.
     * @param tag a string which can identify this task, for debug purpose only
     * @return null if can't schedule async task, otherwise the task itself.
     */
    public static Task exec(ConcurrencyType ct, final Task task, final long backgroundDelay, final long uiDelay, String tag) {
        Assert.notNull(task);
        Assert.isTrue(backgroundDelay >= 0 && uiDelay >= 0);

        final HardReference<Task> aTask = new HardReference<Task>(task);

        task.mSingleThread = (ct == ConcurrencyType.SINGLE);

        // fix: this will improve performance, for UI task bypass thread pool will be quicker.
        if (task instanceof UITask) {
            if (task.isCancelled())
                return task;

            Message msg = mHandler.obtainMessage(MSG_TYPE_CALLBACK, aTask);
            // fix: some overloaded methods pass delay in backgroundDelay.
            // (UITask don't have background job, so it's impossible that caller want background delay)
            // usually only one delay parameter is non-zero, so we add them here to make either of them effective.
            long delay = uiDelay + backgroundDelay;
            mHandler.sendMessageDelayed(msg, delay);  // note: ignore return value here
            return task;
        }

        try {
            final Executor executor = task.mSingleThread ? mExecutor1
                    : (ct == ConcurrencyType.PLOADER ? mExecutorP
                    : (ct == ConcurrencyType.OLOADER ? mExecutorO : mExecutorN));
            task.mFuture = executor.schedule(new Runnable() {
                @Override
                public void run() {
                    Task task = aTask.get();

                    // for debug
                    if (Executor.mDebug)
                        executor.beforeExecute(task.mSeqNo);

                    if (task.isCancelled())
                        return;

                    try {
                        task.execute();
                    } catch (Exception e) {
                        task.mError = e;
                        Logger.w(TAG, e.toString(), e);
                        if (mIsDebugVersion)
                            e.printStackTrace();
                    }

                    if (task.isCancelled())
                        return;

                    Message msg = mHandler.obtainMessage(MSG_TYPE_CALLBACK, aTask);
                    mHandler.sendMessageDelayed(msg, uiDelay);  // note: ignore return value here
                }
            }, backgroundDelay, task.mSeqNo, tag);
            return task;
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
            return null;
        }
    }

    public static void exec(Runnable runnable) {
        exec(runnable, 0);
    }

    /**
     * execute an async task with some delay, use an internal multiple-threaded-pool threads.
     * @param runnable the task's Runnable
     * @param delay delay before actually execute this runnable
     */
    public static void exec(Runnable runnable, final long delay) {
        Assert.notNull(runnable);

        try {
            final Executor executor = mExecutorN;
            executor.schedule(runnable, delay, 0, null);
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
        }
    }

    /**
     * execute an async task, use an internal cached thread pool (no max limits).
     * @param runnable the task's Runnable
     */
    public static void execZForUI(RunnableWithName runnable) {
        Assert.notNull(runnable);

        try {
            mExecutorZForUI.submit(runnable.getRunnable());
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
        }
    }

    /**
     * execute an async task, use an internal cached thread pool (no max limits).
     * @param runnable the task's Runnable
     */
    public static void execZForSDK(RunnableWithName runnable) {
        Assert.notNull(runnable);

        try {
            mExecutorZForSDK.submit(runnable.getRunnable());
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
        }
    }

    /**
     * execute an async task, use an internal cached thread pool (no max limits).
     * @param task the async task
     */
    public static Task execZForSDK(Task task) {
        return execZForSDK(task, 0);
    }

    /**
     * execute an async task, use an internal cached thread pool (no max limits).
     * @param task the async task
     * @param delay before actually execute this runnable
     */
    public static Task execZForSDK(Task task, final long delay) {
        Assert.notNull(task);

        try {
            final HardReference<Task> aTask = new HardReference<Task>(task);

            final ExecutorService executor = mExecutorZForSDK;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    if (delay > 0) {
                        try {
                            Thread.sleep(delay);
                        } catch (InterruptedException e1) {}
                    }

                    Task task = aTask.get();
                    if (task.isCancelled())
                        return;

                    try {
                        task.execute();
                    } catch (Exception e) {
                        task.mError = e;
                        Logger.w(TAG, e.toString(), e);
                        if (mIsDebugVersion)
                            e.printStackTrace();
                    }

                    if (task.isCancelled())
                        return;
                    Message msg = mHandler.obtainMessage(MSG_TYPE_CALLBACK, aTask);
                    mHandler.sendMessage(msg);  // note: ignore return value here
                }
            });
            return task;
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
            return null;
        }
    }

    /**
     * execute an async task from TaskScheduler, use an internal cached thread pool (no max limits).
     * @param runnable the task's Runnable
     */
    public static void execScheduler(RunnableWithName runnable) {
        Assert.notNull(runnable);

        try {
            mScheduler.submit(runnable.getRunnable());
        } catch (RejectedExecutionException e) {
            Logger.w(TAG, e.toString());
        }
    }
}
